package entity;

import lombok.Data;
import utils.FileUtil;
import utils.MathUtil;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.time.LocalTime;
import java.util.*;

/**
 * 神经元类
 *
 * @author liangfuqiang
 * @since 2023-03-14
 */
@Data
public class BPNN implements Serializable {
    /**
     * 每一层数量
     */
    private int[] layerNums;
    /**
     * 权重
     */
    private double[][][] wights;
    /**
     * 训练次数
     */
    private int loop;
    /**
     * 正确值
     */
    private static final double RIGHT = 0.99;
    /**
     * 失败值
     */
    private static final double WRONG = 0.01;
    /**
     * 变化率
     */
    private static final double CHANGE_RATE = 0.025;

    public BPNN(int inputNum, int midNum, int outputNum, int loop) {
        this.loop = loop;
        this.layerNums = new int[midNum + 2];
        this.layerNums[0] = inputNum;
        int length = this.layerNums.length;
        this.layerNums[length - 1] = outputNum;
        Arrays.fill(this.layerNums, 1, length - 1, (int) Math.log(inputNum) + 10);
        this.setWight();
    }

    /**
     * 判断是否有已经训练过的数据
     */
    private void setWight() {
        BPNN read = FileUtil.read();
        if (read != null) {
            this.setWights(read.getWights());
        } else {
            int length = this.layerNums.length;
            this.wights = new double[length - 1][][];
            for (int i = 0; i < length - 1; i++) {
                int row = this.layerNums[i + 1];
                int col = this.layerNums[i];
                this.wights[i] = new double[row][col];
                this.setRandom(this.wights[i], row, col);
            }
        }
    }

    private void setRandom(double[][] part, int row, int col) {
        double range = 1 / Math.sqrt(row);
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                part[i][j] = MathUtil.randomRange(-range, range);
            }
        }
    }

    public void compute() {
        File[] files = new File(FileUtil.RESOURCES + "test").listFiles();
        assert files != null;
        int round = 0;
        long begin = System.currentTimeMillis(), end;
        while (round < this.loop) {
            for (File file : files) {
                this.training(file);
            }
            round++;
            if (round % 100 == 0) {
                FileUtil.save(round, this);
            }
            end = System.currentTimeMillis();
            System.err.println(LocalTime.now() + ": 第 " + round + " 轮训练耗时 " + (end - begin) + " 毫秒");
            begin = end;
        }
    }

    private void training(File file) {
        int layerLength = this.layerNums.length;
        int outputNum = this.layerNums[layerLength - 1];
        double[][][] values = new double[layerLength][][];
        double[][][] deltas = new double[layerLength - 1][][];
        deltas[deltas.length - 1] = new double[outputNum][1];
        //计算输出权重值 color相当于输入
        values[0] = FileUtil.getColor(file);

        for (int i = 1; i < layerLength; i++) {
            //矩阵运算下一层的输出
            values[i] = MathUtil.matrixMultiply(this.wights[i - 1], values[i - 1]);
            MathUtil.setSigmoid(values[i]);
        }
        //计算输出层差值
        int number = CutImage.getNumber(file.getName());
        for (int i = 0; i < outputNum; i++) {
            //目标值 - 实际值
            deltas[deltas.length - 1][i][0] = (number == i ? RIGHT : WRONG) - values[layerLength - 1][i][0];
        }
        //计算输出层权重变化值 -- [右亮差 * 右亮 * (1 - 右亮)] * T(上一层亮度) * α
        MathUtil.changeWight(deltas[deltas.length - 1], values[layerLength - 1], values[layerLength - 2], CHANGE_RATE, this.wights[layerLength - 2]);
        for (int i = layerLength - 2; i > 0; i--) {
            //计算差值 -- T(权重) * 变化量 = 前一个变化量
            deltas[i - 1] = MathUtil.matrixMultiply(MathUtil.matrixTrans(this.wights[i]), deltas[i]);
            MathUtil.changeWight(deltas[i - 1], values[i], values[i - 1], CHANGE_RATE, this.wights[i - 1]);
        }
    }

    /**
     * 估计算法
     *
     * @param image 图片文件
     * @return 估算数据
     */
    public static List<Map.Entry<Integer, Double>> estimate(BufferedImage image) {
        double[][] color = FileUtil.getColor(image);
        BPNN bpnn = FileUtil.read();
        if (bpnn == null) {
            throw new RuntimeException("未发现训练文件");
        }
        return bpnn.estimate(color);
    }

    public static List<Map.Entry<Integer, Double>> estimate(File file) {
        try {
            return estimate(ImageIO.read(file));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private List<Map.Entry<Integer, Double>> estimate(double[][] input) {
        int layerLength = this.layerNums.length;
        double[][][] values = new double[layerLength][][];
        //计算输出权重值 color相当于输入
        values[0] = input;

        for (int i = 1; i < layerLength; i++) {
            //矩阵运算下一层的输出
            values[i] = MathUtil.matrixMultiply(this.wights[i - 1], values[i - 1]);
            MathUtil.setSigmoid(values[i]);
        }
        List<Map.Entry<Integer, Double>> res = new ArrayList<>();
        for (int i = 0; i < values[layerLength - 1].length; i++) {
            res.add(new AbstractMap.SimpleEntry<>(i, values[layerLength - 1][i][0]));
        }
        return res;
    }
}
